/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package com.google.android.accessibility.compositor;

import android.content.Context;
import androidx.annotation.Nullable;
import com.google.android.accessibility.utils.LocaleUtils;
import com.google.android.accessibility.utils.PackageManagerUtils;
import com.google.android.accessibility.utils.SpeechCleanupUtils;
import com.google.android.accessibility.utils.parsetree.ParseTree;
import com.google.android.libraries.accessibility.utils.log.LogUtils;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

/** A VariableDelegate that maps data from EventInterpretation. */
class InterpretationVariables implements ParseTree.VariableDelegate {

  private static final String TAG = "InterpretationVariables";

  // IDs of variables.
  private static final int EVENT_TEXT_OR_DESCRIPTION = 5000;
  private static final int EVENT_REMOVED_TEXT = 5001;
  private static final int EVENT_ADDED_TEXT = 5002;
  private static final int EVENT_TRAVERSED_TEXT = 5003;
  private static final int EVENT_DESELECTED_TEXT = 5004;
  private static final int EVENT_SELECTED_TEXT = 5005;
  private static final int EVENT_LAST_WORD = 5006;
  private static final int EVENT_IS_CUT = 5007;
  private static final int EVENT_IS_PASTE = 5008;
  private static final int EVENT_HINT_TYPE = 5009;
  private static final int EVENT_HINT_FORCE_AUDIO_PLAYBACK_ACTIVE = 5010;
  private static final int EVENT_HINT_FORCE_MICROPHONE_ACTIVE = 5011;
  private static final int EVENT_HINT_TEXT = 5012;
  private static final int EVENT_IS_FORCED_FEEDBACK_AUDIO_PLAYBACK_ACTIVE = 5013;
  private static final int EVENT_IS_FORCED_FEEDBACK_MICROPHONE_ACTIVE = 5014;
  private static final int EVENT_IS_FORCED_FEEDBACK_SSB_ACTIVE = 5015;
  private static final int EVENT_NODE_MULTIPLE_SWITCH_ACCESS_ACTIONS = 5016;
  private static final int EVENT_IS_INITIAL_FOCUS = 5017;
  private static final int EVENT_IS_USER_NAVIGATION = 5018;

  // IDs of enums.
  private static final int ENUM_HINT_TYPE = 5200;

  private final Context mContext;
  private final ParseTree.VariableDelegate mParent;
  private final EventInterpretation mEventInterpretation;
  private @Nullable final Locale mUserPreferredLocale;

  /**
   * Constructs InterpretationVariables, which contains context variables to help generate feedback
   * for an accessibility event. Caller must call {@code cleanup()} when done with this object.
   */
  InterpretationVariables(
      Context context,
      ParseTree.VariableDelegate parent,
      EventInterpretation eventInterpreted,
      @Nullable Locale userPreferredLocale) {
    mUserPreferredLocale = userPreferredLocale;
    mContext = context;
    mParent = parent;
    mEventInterpretation = eventInterpreted;
    // TODO: We should have a variable mLocale like we have in NodeVariables to
    // capture the locale if the event is from IME. gBoard uses Nodevariables to wrap the locale
    // for keyboard input. Currently a few IMEs do not send the source with the event.
    // So it is not clear how to detect if the event was generated by IME.
    // The captured locale should be wrapped around event text and event description.
  }

  @Override
  public void cleanup() {
    if (mParent != null) {
      mParent.cleanup();
    }
  }

  @Override
  public boolean getBoolean(int variableId) {
    switch (variableId) {
      case EVENT_IS_CUT:
        return safeTextInterpretation().getIsCutAction();
      case EVENT_IS_PASTE:
        return safeTextInterpretation().getIsPasteAction();
      case EVENT_HINT_FORCE_AUDIO_PLAYBACK_ACTIVE:
        {
          @Nullable HintEventInterpretation hintInterp = mEventInterpretation.getHint();
          return (hintInterp != null) && hintInterp.getForceFeedbackAudioPlaybackActive();
        }
      case EVENT_HINT_FORCE_MICROPHONE_ACTIVE:
        {
          @Nullable HintEventInterpretation hintInterp = mEventInterpretation.getHint();
          return (hintInterp != null) && hintInterp.getForceFeedbackMicrophoneActive();
        }
      case EVENT_IS_FORCED_FEEDBACK_AUDIO_PLAYBACK_ACTIVE:
        {
          return safeAccessibilityFocusInterpretation().getForceFeedbackAudioPlaybackActive();
        }
      case EVENT_IS_FORCED_FEEDBACK_MICROPHONE_ACTIVE:
        {
          return safeAccessibilityFocusInterpretation().getForceFeedbackMicrophoneActive();
        }
      case EVENT_IS_FORCED_FEEDBACK_SSB_ACTIVE:
        {
          return safeAccessibilityFocusInterpretation().getForceFeedbackSsbActive();
        }
      case EVENT_NODE_MULTIPLE_SWITCH_ACCESS_ACTIONS:
        {
          return (mEventInterpretation != null)
              && (mEventInterpretation.getHasMultipleSwitchAccessActions());
        }
      case EVENT_IS_INITIAL_FOCUS:
        {
          return safeAccessibilityFocusInterpretation().getIsInitialFocusAfterScreenStateChange();
        }
      case EVENT_IS_USER_NAVIGATION:
        {
          return safeAccessibilityFocusInterpretation().getIsNavigateByUser();
        }
      default:
        return mParent.getBoolean(variableId);
    }
  }

  @Override
  public int getInteger(int variableId) {
    return mParent.getInteger(variableId);
  }

  @Override
  public double getNumber(int variableId) {
    return mParent.getNumber(variableId);
  }

  @Override
  public @Nullable CharSequence getString(int variableId) {
    // TODO: Remove collapseRepeatedCharactersAndCleanUp() from VariableDelegate classes. Instead,
    // apply collapseRepeatedCharactersAndCleanUp() to Compositor ttsOutput result whenever
    // Compositor output ttsOutputClean returns true (default is true).
    // TODO: Use spans to mark which parts of composed text are already clean (or should never be
    // cleaned).
    CharSequence text = getStringInternal(variableId);
    if (text == null) {
      return mParent.getString(variableId);
    } else {
      return SpeechCleanupUtils.collapseRepeatedCharactersAndCleanUp(mContext, text);
    }
  }

  private @Nullable CharSequence getStringInternal(int variableId) {
    switch (variableId) {
      case EVENT_TEXT_OR_DESCRIPTION:
        return safeTextInterpretation().getTextOrDescription();
      case EVENT_REMOVED_TEXT:
        return safeTextInterpretation().getRemovedText();
      case EVENT_ADDED_TEXT:
        return safeTextInterpretation().getAddedText();
      case EVENT_TRAVERSED_TEXT:
        {
          CharSequence traversedText = safeTextInterpretation().getTraversedText();
          /**
           * Wrap the text with user preferred locale changed using language switcher, with an
           * exception for all talkback created events. As talkback text is always in the system
           * language.
           */
          if (PackageManagerUtils.isTalkBackPackage(mEventInterpretation.getPackageName())) {
            return traversedText;
          }
          return LocaleUtils.wrapWithLocaleSpan(traversedText, mUserPreferredLocale);
        }
      case EVENT_DESELECTED_TEXT:
        return safeTextInterpretation().getDeselectedText();
      case EVENT_SELECTED_TEXT:
        return safeTextInterpretation().getSelectedText();
      case EVENT_LAST_WORD:
        return safeTextInterpretation().getInitialWord();
      case EVENT_HINT_TEXT:
        {
          @Nullable HintEventInterpretation hintInterp = mEventInterpretation.getHint();
          return (hintInterp == null) ? "" : hintInterp.getText();
        }
      default:
        return null;
    }
  }

  @Override
  public int getEnum(int variableId) {
    switch (variableId) {
      case EVENT_HINT_TYPE:
        {
          @Nullable HintEventInterpretation hintInterp = mEventInterpretation.getHint();
          return (hintInterp == null)
              ? HintEventInterpretation.HINT_TYPE_NONE
              : hintInterp.getHintType();
        }
      default:
        return mParent.getEnum(variableId);
    }
  }

  @Override
  public @Nullable ParseTree.VariableDelegate getReference(int variableId) {
    return mParent.getReference(variableId);
  }

  @Override
  public int getArrayLength(int variableId) {
    return mParent.getArrayLength(variableId);
  }

  @Override
  public @Nullable CharSequence getArrayStringElement(int variableId, int index) {
    return mParent.getArrayStringElement(variableId, index);
  }

  /** Caller must call VariableDelegate.cleanup() on returned instance. */
  @Override
  public @Nullable ParseTree.VariableDelegate getArrayChildElement(int variableId, int index) {
    return mParent.getArrayChildElement(variableId, index);
  }

  private TextEventInterpretation safeTextInterpretation() {
    if (mEventInterpretation != null) {
      TextEventInterpretation textInterpretation = mEventInterpretation.getText();
      if (textInterpretation != null) {
        return textInterpretation;
      }
    }
    LogUtils.w(TAG, "Falling back to safe TextEventInterpretation");
    return new TextEventInterpretation(Compositor.EVENT_UNKNOWN);
  }

  private AccessibilityFocusEventInterpretation safeAccessibilityFocusInterpretation() {
    if (mEventInterpretation != null) {
      AccessibilityFocusEventInterpretation a11yFocusInterpretation =
          mEventInterpretation.getAccessibilityFocusInterpretation();
      if (a11yFocusInterpretation != null) {
        return a11yFocusInterpretation;
      }
    }
    LogUtils.w(TAG, "Falling back to safe AccessibilityFocusEventInterpretation");
    return new AccessibilityFocusEventInterpretation(Compositor.EVENT_UNKNOWN);
  }

  static void declareVariables(ParseTree parseTree) {

    // Enum values for hint type.
    Map<Integer, String> hintTypes = new HashMap<>();
    hintTypes.put(HintEventInterpretation.HINT_TYPE_NONE, "none");
    hintTypes.put(HintEventInterpretation.HINT_TYPE_ACCESSIBILITY_FOCUS, "access_focus");
    hintTypes.put(HintEventInterpretation.HINT_TYPE_INPUT_FOCUS, "input_focus");
    hintTypes.put(HintEventInterpretation.HINT_TYPE_SCREEN, "screen");
    hintTypes.put(HintEventInterpretation.HINT_TYPE_SELECTOR, "selector");
    parseTree.addEnum(ENUM_HINT_TYPE, hintTypes);

    parseTree.addStringVariable("event.textOrDescription", EVENT_TEXT_OR_DESCRIPTION);
    parseTree.addStringVariable("event.removedText", EVENT_REMOVED_TEXT);
    parseTree.addStringVariable("event.addedText", EVENT_ADDED_TEXT);
    parseTree.addStringVariable("event.traversedText", EVENT_TRAVERSED_TEXT);
    parseTree.addStringVariable("event.deselectedText", EVENT_DESELECTED_TEXT);
    parseTree.addStringVariable("event.selectedText", EVENT_SELECTED_TEXT);
    parseTree.addStringVariable("event.initialWord", EVENT_LAST_WORD);
    parseTree.addBooleanVariable("event.isCut", EVENT_IS_CUT);
    parseTree.addBooleanVariable("event.isPaste", EVENT_IS_PASTE);
    parseTree.addEnumVariable("event.hintType", EVENT_HINT_TYPE, ENUM_HINT_TYPE);
    parseTree.addBooleanVariable(
        "event.hintForceAudioPlaybackActive", EVENT_HINT_FORCE_AUDIO_PLAYBACK_ACTIVE);
    parseTree.addBooleanVariable(
        "event.hintForceMicrophoneActive", EVENT_HINT_FORCE_MICROPHONE_ACTIVE);
    parseTree.addStringVariable("event.hintText", EVENT_HINT_TEXT);
    parseTree.addBooleanVariable(
        "event.isForcedFeedbackAudioPlaybackActive",
        EVENT_IS_FORCED_FEEDBACK_AUDIO_PLAYBACK_ACTIVE);
    parseTree.addBooleanVariable(
        "event.isForcedFeedbackMicrophoneActive", EVENT_IS_FORCED_FEEDBACK_MICROPHONE_ACTIVE);
    parseTree.addBooleanVariable(
        "event.isForcedFeedbackSsbActive", EVENT_IS_FORCED_FEEDBACK_SSB_ACTIVE);
    parseTree.addBooleanVariable("event.isInitialFocus", EVENT_IS_INITIAL_FOCUS);
    parseTree.addBooleanVariable("event.isNavigateByUser", EVENT_IS_USER_NAVIGATION);
    parseTree.addBooleanVariable(
        "event.hasMultipleSwitchAccessActions", EVENT_NODE_MULTIPLE_SWITCH_ACCESS_ACTIONS);
  }
}
